Zer0pts Syscall 777 Write Up
Details:
Jeopardy style CTF
Category: Reversing
Write up:
This challenge had a 64 bit binary that when run would ask for a flag before printing out wrong if it was incorrect. Pulling the file up in a decompiler I saw the below main function:
__int64 __fastcall main(int a1, char **a2, char **a3)
{
__int64 v3; // rcx
unsigned int v4; // er12
int *v5; // rdi
__int64 i; // rbx
int v8[14]; // [rsp+0h] [rbp-68h] BYREF
unsigned __int64 v9; // [rsp+38h] [rbp-30h]
v3 = 14LL;
v4 = 1;
v9 = __readfsqword(0x28u);
v5 = v8;
while ( v3 )
{
*v5++ = 0;
--v3;
}
__printf_chk(1LL, "FLAG: ", a3);
if ( (unsigned int)__isoc99_scanf("%55s", v8) == 1 )
{
for ( i = 1LL; i != 15; ++i )
{
syscall(
777LL,
(unsigned int)v8[i - 1],
(unsigned int)v8[(int)i % 14],
(unsigned int)v8[((int)i + 1) % 14],
(unsigned int)v8[((int)i + 2) % 14]);
v4 = *__errno_location();
if ( v4 == 1 )
{
puts("Wrong...");
return v4;
}
}
v4 = 0;
puts("Correct!");
}
return v4;
}
The program was using the system call to check the flag, however syscall 777 was not a valid call. I then tried an strace and saw something interesting:
strace ./chall
execve("./chall", ["./chall"], 0x7ffe48ddcbd0 /* 48 vars */) = 0
brk(NULL) = 0x55cd8c957000
access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=84692, ...}) = 0
mmap(NULL, 84692, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7fd7bc273000
close(3) = 0
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1\1\3\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0@n\2\0\0\0\0\0"..., 832) = 832
fstat(3, {st_mode=S_IFREG|0755, st_size=1839792, ...}) = 0
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fd7bc271000
mmap(NULL, 1852680, PROT_READ, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7fd7bc0ac000
mprotect(0x7fd7bc0d1000, 1662976, PROT_NONE) = 0
mmap(0x7fd7bc0d1000, 1355776, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x25000) = 0x7fd7bc0d1000
mmap(0x7fd7bc21c000, 303104, PROT_READ, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x170000) = 0x7fd7bc21c000
mmap(0x7fd7bc267000, 24576, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1ba000) = 0x7fd7bc267000
mmap(0x7fd7bc26d000, 13576, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7fd7bc26d000
close(3) = 0
arch_prctl(ARCH_SET_FS, 0x7fd7bc272580) = 0
mprotect(0x7fd7bc267000, 12288, PROT_READ) = 0
mprotect(0x55cd8c82c000, 4096, PROT_READ) = 0
mprotect(0x7fd7bc2b2000, 4096, PROT_READ) = 0
munmap(0x7fd7bc273000, 84692) = 0
prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) = 0
prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, {len=205, filter=0x7ffd4924fb10}) = 0
fstat(1, {st_mode=S_IFCHR|0600, st_rdev=makedev(0x88, 0), ...}) = 0
brk(NULL) = 0x55cd8c957000
brk(0x55cd8c978000) = 0x55cd8c978000
fstat(0, {st_mode=S_IFCHR|0600, st_rdev=makedev(0x88, 0), ...}) = 0
write(1, "FLAG: ", 6FLAG: ) = 6
read(0,
We were calling something that was setting a filter value. Looking through the docs for prctl and SECCOMP I found out that this did indeed set what syscall's were or were not accepted and that the filter = pointed to the sock_filter structure. Upon more googling we realized that the structure was:
struct sock_filter { /* Filter block */
__u16 code; /* Actual filter code */
__u8 jt; /* Jump true */
__u8 jf; /* Jump false */
__u32 k; /* Generic multiuse field */
};
From here we then decided to do dynamic analysis, I found the prctl function and set a breakpoint, once the filter was set I went to the memory the pointer pointed at and copied the relevent bytes:
20 00 00 00 00 00 00 00 15 00 00 C8 09 03 00 00
20 00 00 00 10 00 00 00 54 00 00 00 FF 00 00 00
35 00 C7 00 80 00 00 00 20 00 00 00 10 00 00 00
74 00 00 00 08 00 00 00 54 00 00 00 FF 00 00 00
35 00 C3 00 80 00 00 00 20 00 00 00 10 00 00 00
74 00 00 00 10 00 00 00 54 00 00 00 FF 00 00 00
35 00 BF 00 80 00 00 00 20 00 00 00 10 00 00 00
74 00 00 00 18 00 00 00 54 00 00 00 FF 00 00 00
35 00 BB 00 80 00 00 00 20 00 00 00 10 00 00 00
02 00 00 00 00 00 00 00 20 00 00 00 18 00 00 00
61 00 00 00 00 00 00 00 AC 00 00 00 00 00 00 00
02 00 00 00 01 00 00 00 20 00 00 00 20 00 00 00
61 00 00 00 01 00 00 00 AC 00 00 00 00 00 00 00
02 00 00 00 02 00 00 00 20 00 00 00 28 00 00 00
61 00 00 00 02 00 00 00 AC 00 00 00 00 00 00 00
02 00 00 00 03 00 00 00 60 00 00 00 00 00 00 00
61 00 00 00 01 00 00 00 0C 00 00 00 00 00 00 00
61 00 00 00 02 00 00 00 0C 00 00 00 00 00 00 00
61 00 00 00 03 00 00 00 0C 00 00 00 00 00 00 00
02 00 00 00 04 00 00 00 60 00 00 00 00 00 00 00
61 00 00 00 01 00 00 00 1C 00 00 00 00 00 00 00
61 00 00 00 02 00 00 00 0C 00 00 00 00 00 00 00
61 00 00 00 03 00 00 00 1C 00 00 00 00 00 00 00
02 00 00 00 05 00 00 00 60 00 00 00 00 00 00 00
61 00 00 00 01 00 00 00 0C 00 00 00 00 00 00 00
61 00 00 00 02 00 00 00 1C 00 00 00 00 00 00 00
61 00 00 00 03 00 00 00 1C 00 00 00 00 00 00 00
02 00 00 00 06 00 00 00 60 00 00 00 00 00 00 00
61 00 00 00 01 00 00 00 1C 00 00 00 00 00 00 00
61 00 00 00 02 00 00 00 1C 00 00 00 00 00 00 00
61 00 00 00 03 00 00 00 0C 00 00 00 00 00 00 00
02 00 00 00 07 00 00 00 60 00 00 00 04 00 00 00
61 00 00 00 05 00 00 00 4C 00 00 00 00 00 00 00
02 00 00 00 08 00 00 00 60 00 00 00 06 00 00 00
61 00 00 00 07 00 00 00 5C 00 00 00 00 00 00 00
61 00 00 00 08 00 00 00 AC 00 00 00 00 00 00 00
02 00 00 00 08 00 00 00 60 00 00 00 05 00 00 00
61 00 00 00 06 00 00 00 4C 00 00 00 00 00 00 00
02 00 00 00 09 00 00 00 60 00 00 00 07 00 00 00
61 00 00 00 04 00 00 00 5C 00 00 00 00 00 00 00
61 00 00 00 09 00 00 00 AC 00 00 00 00 00 00 00
02 00 00 00 09 00 00 00 60 00 00 00 06 00 00 00
61 00 00 00 07 00 00 00 4C 00 00 00 00 00 00 00
02 00 00 00 0A 00 00 00 60 00 00 00 04 00 00 00
61 00 00 00 05 00 00 00 5C 00 00 00 00 00 00 00
61 00 00 00 0A 00 00 00 AC 00 00 00 00 00 00 00
02 00 00 00 0A 00 00 00 60 00 00 00 07 00 00 00
61 00 00 00 04 00 00 00 4C 00 00 00 00 00 00 00
02 00 00 00 0B 00 00 00 60 00 00 00 05 00 00 00
61 00 00 00 06 00 00 00 5C 00 00 00 00 00 00 00
61 00 00 00 0B 00 00 00 AC 00 00 00 00 00 00 00
02 00 00 00 0B 00 00 00 60 00 00 00 08 00 00 00
My teammate then wrote the following go program to parse the bytes and using the built in BPF library in go to convert the bytes to the BPF assembly.
package main;
import (
"bufio"
"encoding/binary"
"encoding/hex"
"encoding/json"
"fmt"
"golang.org/x/net/bpf"
"os"
"regexp"
)
func main() {
arg := 1;
file, _ := os.Open("./bpf.txt")
raw := getInstructions(file);
instructions, allDecoded := bpf.Disassemble(raw);
for i := range instructions {
if arg == 0 {
xType := fmt.Sprintf("%T", instructions[i])
print(xType);
print(" | ");
j, _ := json.Marshal(instructions[i]);
fmt.Printf("%s\n", string(j));
} else {
fmt.Printf("%+v\n", instructions[i]);
}
}
print("---------\n");
print(allDecoded);
print("\n")
}
func getInstructions(file *os.File) []bpf.RawInstruction {
instructions := make([]bpf.RawInstruction, 0)
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := scanner.Text()
bytes := regexp.MustCompile("[^\\s]+").FindAllString(line, -1)
instructions = append(instructions, parseInstruction(bytes[0:8]))
instructions = append(instructions, parseInstruction(bytes[8:]))
}
return instructions
}
func parseInstruction(strings []string) bpf.RawInstruction {
fmt.Println(strings)
opStr := strings[1] + strings[0]
opBytes, _ := hex.DecodeString(opStr)
op := binary.BigEndian.Uint16(opBytes)
jtStr := strings[3]
jtBytes, _ := hex.DecodeString(jtStr)
jfStr := strings[4]
jfBytes, _ := hex.DecodeString(jfStr)
kStr := strings[7] + strings[6] + strings[5] + strings[4]
kBytes, _ := hex.DecodeString(kStr)
k := binary.BigEndian.Uint32(kBytes)
instruction := bpf.RawInstruction{Op: op, Jt: jtBytes[0], Jf: jfBytes[0], K: k}
fmt.Printf("%+v\n", instruction)
return instruction
}
When run the output was:
[20 00 00 00 00 00 00 00]
{Op:32 Jt:0 Jf:0 K:0}
...
ld [0]
jeq #777,200,9
ld [16]
and #255
jlt #128,128
ld [16]
rsh #8
and #255
jlt #128,128
ld [16]
rsh #16
and #255
jlt #128,128
ld [16]
rsh #24
and #255
jlt #128,128
ld [16]
st M[0]
ld [24]
ldx M[0]
xor x
st M[1]
ld [32]
ldx M[1]
xor x
st M[2]
ld [40]
ldx M[2]
xor x
st M[3]
ld M[0]
ldx M[1]
add x
ldx M[2]
add x
ldx M[3]
add x
st M[4]
ld M[0]
ldx M[1]
sub x
ldx M[2]
add x
ldx M[3]
sub x
st M[5]
ld M[0]
ldx M[1]
add x
ldx M[2]
sub x
ldx M[3]
sub x
st M[6]
ld M[0]
ldx M[1]
sub x
ldx M[2]
sub x
ldx M[3]
add x
st M[7]
ld M[4]
ldx M[5]
or x
st M[8]
ld M[6]
ldx M[7]
and x
ldx M[8]
xor x
st M[8]
ld M[5]
ldx M[6]
or x
st M[9]
ld M[7]
ldx M[4]
and x
ldx M[9]
xor x
st M[9]
ld M[6]
ldx M[7]
or x
st M[10]
ld M[4]
ldx M[5]
and x
ldx M[10]
xor x
st M[10]
ld M[7]
ldx M[4]
or x
st M[11]
ld M[5]
ldx M[6]
and x
ldx M[11]
xor x
st M[11]
ld M[8]
---------
true
We then went through and commented the assembly to figure out what each component did:
ld [0]
jneq #777,200 ; Syscall # != 777 -> exit
ld [16]
and #255
jge #128,199 ; if char 1 > 128 exit
ld [16]
rsh #8
and #255
jge #128,195 ; if char 2 > 128 exit
ld [16]
rsh #16
and #255
jge #128,191 ; if char 3 > 128 exit
ld [16]
rsh #24
and #255
jge #128,187 ; if char 4 > 128 exit
ld [16] ; A = arg[0]
st M[0] ; M[0] = arg[0]
ld [24]
ldx M[0]
xor x
st M[1] ; M[1] = M[0] xor arg[1]
ld [32]
ldx M[1]
xor x
st M[2] ; M[2] = arg[2] xor M[1]
ld [40]
ldx M[2]
xor x
st M[3] ; M[3] = arg[3] xor M[2]
ld M[0]
ldx M[1]
add x
ldx M[2]
add x
ldx M[3]
add x
st M[4] ; M[4] = M[0] + M[1] + M[2] + M[3]
ld M[0]
ldx M[1]
sub x
ldx M[2]
add x
ldx M[3]
sub x
st M[5] ; M[5] = M[0] - M[1] + M[2] - M[3]
ld M[0]
ldx M[1]
add x
ldx M[2]
sub x
ldx M[3]
sub x
st M[6] ; M[6] = M[0] + M[1] - M[2] - M[3]
ld M[0]
ldx M[1]
sub x
ldx M[2]
sub x
ldx M[3]
add x
st M[7] ; M[7] = M[0] - M[1] - M[2] + M[3]
ld M[4]
ldx M[5]
or x
st M[8]
ld M[6]
ldx M[7]
and x
ldx M[8]
xor x
st M[8] ; M[8] = (M[4] OR M[5]) XOR (M[6] AND M[7])
ld M[5]
ldx M[6]
or x
st M[9]
ld M[7]
ldx M[4]
and x
ldx M[9]
xor x
st M[9] ; M[9] = (M[5] OR M[6]) XOR (M[7] AND M[4])
ld M[6]
ldx M[7]
or x
st M[10]
ld M[4]
ldx M[5]
and x
ldx M[10]
xor x
st M[10] ; M[10] = (M[6] OR M[7]) XOR (M[4] AND M[5])
ld M[7]
ldx M[4]
or x
st M[11]
ld M[5]
ldx M[6]
and x
ldx M[11]
xor x
st M[11] ; M[11] = (M[7] OR M[4]) XOR (M[5] AND M[6])
ld M[8] ; Check M[8]
jeq #4127179254,37
jeq #1933881070,20
jeq #4255576062,31
jeq #1670347938,10
jeq #2720551936,15
jeq #2307981054,26
jeq #2673307092,29
jeq #4139379682,10
jeq #4192373742,15
jeq #530288564,20
jeq #4025255646,29
jeq #3747612986,14
jeq #3098492862,3
jeq #2130820044,14
ld M[9] ; Check M[9]
jeq #4056898606,50,84
ld M[9]
jeq #3064954302,32,82
ld M[9]
jeq #3602496994,24,80
ld M[9]
jeq #1627051272,34,78
ld M[9]
jeq #2002783966,34,76
ld M[9]
jeq #4088827598,28,74
ld M[9]
jeq #1340672294,36,72
ld M[9]
jeq #2115580844,12,70
ld M[9]
jeq #530288564,36,68
ld M[9]
jeq #3415533530,28,66
ld M[9]
jeq #3116543486,10,64
ld M[9]
jeq #251771212,14,62
ld M[9]
jeq #4126139894,20,60
ld M[9]
jeq #2813168974,6,58
ld M[10] ; Check M[10]
jeq #2130523044,46,56
ld M[10]
jeq #3606265306,36,54
ld M[10]
jeq #3151668710,38,52
ld M[10]
jeq #614968622,34,50
ld M[10]
jeq #3086875838,42,48
ld M[10]
jeq #251771212,16,46
ld M[10]
jeq #3015552726,18,44
ld M[10]
jeq #1627379644,34,42
ld M[10]
jeq #1601724370,12,40
ld M[10]
jeq #665780030,18,38
ld M[10]
jeq #3281895882,14,36
ld M[10]
jeq #1301225350,30,34
ld M[10]
jeq #2583645294,20,32
ld M[10]
jeq #3917315412,6,30
ld M[11] ; Check M[11]
jeq #2673307092,27,28
ld M[11]
jeq #1532821474,25,26
ld M[11]
jeq #3119098870,23,24
ld M[11]
jeq #3917315412,21,22
ld M[11]
jeq #2174343406,19,20
ld M[11]
jeq #666819390,17,18
ld M[11]
jeq #4143147994,15,16
ld M[11]
jeq #1827055294,13,14
ld M[11]
jeq #4290701286,11,12
ld M[11]
jeq #197094626,9,10
ld M[11]
jeq #2145762244,7,8
ld M[11]
jeq #2720880308,5,6
ld M[11]
jeq #3120414398,3,4
ld M[11]
jeq #3708166042,1,2
ret #2147418112 ; Syscall# != 777
ret #327680 ; Success
ret #327681 ; Error
after a bit we found out that:
M[0] = arg[0]
M[1] = M[0] XOR arg[1]
M[2] = arg[2] XOR M[1]
M[3] = arg[3] XOR M[2]
M[4] = M[0] + M[1] + M[2] + M[3]
M[5] = M[0] - M[1] + M[2] - M[3]
M[6] = M[0] + M[1] - M[2] - M[3]
M[7] = M[0] - M[1] - M[2] + M[3]
M[8] = (M[4] OR M[5]) XOR (M[6] AND M[7])
M[9] = (M[5] OR M[6]) XOR (M[7] AND M[4])
M[10] = (M[6] OR M[7]) XOR (M[4] AND M[5])
M[11] = (M[7] OR M[4]) XOR (M[5] AND M[6])
and that these values had to have the same values for each line
M[8] M[9] M[10] M[11]
---------------------------------------------------------
4127179254 4126139894 3086875838 3120414398
1933881070 2002783966 1601724370 1532821474
4255576062 3116543486 3151668710 4290701286
1670347938 4056898606 2583645294 197094626
2720551936 1627051272 1627379644 2720880308
2307981054 3415533530 3281895882 2174343406
2673307092 251771212 251771212 2673307092
4139379682 3602496994 3606265306 4143147994
4192373742 4088827598 3015552726 3119098870
530288564 530288564 3917315412 3917315412
4025255646 2813168974 614968622 1827055294
3747612986 1340672294 1301225350 3708166042
3098492862 3064954302 3086875838 3120414398
2130820044 2115580844 2130523044 2145762244
We then decided to make a python script that would use z3 to solve for the different chunks. From the main function we had found earlier we knew that each input (arg[x]) would be equal to 4 characters. After adding all the constraints the python function then printed out the results in ascii format rather than as integers:
from z3 import *
# instantiate solver
s = Solver()
# create arguments
args = [BitVec(f'args[{i}]', 32) for i in range(0,14)]
# Add char constraints
for i in range(0, len(args)):
# last character
s.add(((args[i]) & 0xff) <= 126)
# second to last character
s.add(((args[i] >> 8) & 0xff) <= 126)
# third from last character
s.add(((args[i] >> 16) & 0xff) <= 126)
# fourth from last character
s.add(((args[i] >> 24) & 0xff) <= 126)
# add constraints
def solve(j):
M = [BitVec(f"M[{j}][{i}]", 32) for i in range(0, 12)]
s.add(M[0] == args[j])
s.add(M[1] == M[0] ^ args[j+1])
s.add(M[2] == M[1] ^ args[j+2])
s.add(M[3] == M[2] ^ args[j+3])
s.add(M[4] == M[0] + M[1] + M[2] + M[3])
s.add(M[5] == M[0] - M[1] + M[2] - M[3])
s.add(M[6] == M[0] + M[1] - M[2] - M[3])
s.add(M[7] == M[0] - M[1] - M[2] + M[3])
s.add(M[8] == (M[4] | M[5]) ^ (M[6] & M[7]))
s.add(M[9] == (M[5] | M[6]) ^ (M[7] & M[4]))
s.add(M[10] == (M[6] | M[7]) ^ (M[4] & M[5]))
s.add(M[11] == (M[7] | M[4]) ^ (M[5] & M[6]))
# adds row checks
s.add(Or(
And(M[8] == 4127179254, M[9] == 4126139894, M[10] == 3086875838, M[11] == 3120414398),
And(M[8] == 1933881070, M[9] == 2002783966, M[10] == 1601724370, M[11] == 1532821474),
And(M[8] == 4255576062, M[9] == 3116543486, M[10] == 3151668710, M[11] == 4290701286),
And(M[8] == 1670347938, M[9] == 4056898606, M[10] == 2583645294, M[11] == 197094626),
And(M[8] == 2720551936, M[9] == 1627051272, M[10] == 1627379644, M[11] == 2720880308),
And(M[8] == 2307981054, M[9] == 3415533530, M[10] == 3281895882, M[11] == 2174343406),
And(M[8] == 2673307092, M[9] == 251771212, M[10] == 251771212, M[11] == 2673307092),
And(M[8] == 4139379682, M[9] == 3602496994, M[10] == 3606265306, M[11] == 4143147994),
And(M[8] == 4192373742, M[9] == 4088827598, M[10] == 3015552726, M[11] == 3119098870),
And(M[8] == 530288564, M[9] == 530288564, M[10] == 3917315412, M[11] == 3917315412),
And(M[8] == 4025255646, M[9] == 2813168974, M[10] == 614968622, M[11] == 1827055294),
And(M[8] == 3747612986, M[9] == 1340672294, M[10] == 1301225350, M[11] == 3708166042),
And(M[8] == 3098492862, M[9] == 3064954302, M[10] == 3086875838, M[11] == 3120414398),
And(M[8] == 2130820044, M[9] == 2115580844, M[10] == 2130523044, M[11] == 2145762244)
))
# swaps characters around
def swap32(x):
return (((x << 24) & 0xFF000000) |
((x << 8) & 0x00FF0000) |
((x >> 8) & 0x0000FF00) |
((x >> 24) & 0x000000FF))
# prints the solution as ascii
def printAsAscii(num):
num = swap32(num)
hexStr = '{:08x}'.format(num)
bytes_object = bytes.fromhex(hexStr)
ascii_string = bytes_object.decode("ASCII")
print(ascii_string)
# calls solve function for each argument
for i in range(0, len(args)-3):
solve(i)
# solves the equation
print(s.check())
print(s.assertions())
m = s.model()
for d in m.decls():
if "args" in d.name():
print("%s = %s" % (d.name(), m[d]))
printAsAscii(m[d].as_long())
After running the program we got:
args[13] = 1832088435
ss3m
args[3] = 2071164016
pts{
args[2] = 812803450
zer0
args[7] = 1601450859
k3t_
args[10] = 878670899
3t_4
args[11] = 1752445038
n0th
args[4] = 1802646338
B3rk
args[5] = 2033413171
3l3y
args[6] = 1664372831
_P4c
args[9] = 1497002547
3r:Y
args[8] = 1953247558
F1lt
args[12] = 878670387
3r_4
args[1] = 0
args[0] = 2105109602
bly}
After assembling this we got that the flag was:
zer0pts{B3rk3l3y_P4ck3t_F1lt3r:Y3t_4n0th3r_4ss3mbly}
Lots of thanks to NodeDigital for working with me on the go file and commenting of the assembly.